package fr.utbm.to52.android_bibtex.model.entry;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import my.javax.swing.event.EventListenerList;
import my.util.MyCollections;
import my.util.MyObjects;
import fr.utbm.to52.android_bibtex.model.spec.EntryDefinition;
import fr.utbm.to52.android_bibtex.model.spec.EntryDefinitionEvent;
import fr.utbm.to52.android_bibtex.model.spec.EntryDefinitionListener;
import fr.utbm.to52.android_bibtex.model.spec.FieldSpecification;

public class Entry implements EntryDefinitionListener, FieldListener {

	private final EntryDefinition definition;
	public static final String DEFINITION_PROPERTY_NAME = "definition";

	private String key;
	public static final String KEY_PROPERTY_NAME = "key";

	/** key: {@link Field#getSpecification()} */
	private final Map<FieldSpecification, Field> fields = new HashMap<FieldSpecification, Field>();
	public static final String FIELDS_PROPERTY_NAME = "fields";

	private final EventListenerList listeners = new EventListenerList();

	public Entry(EntryDefinition definition) {
		assert definition!=null;

		this.definition = definition;
	}

	public Entry(EntryDefinition definition, String key) {
		assert definition!=null;
		assert key!=null && !key.isEmpty();

		this.definition = definition;
		this.key = key;
	}

	public EntryDefinition getDefinition() {
		return this.definition;
	}

	public String getKey() {
		return this.key;
	}

	public void setKey(String key) {
		assert key!=null && !key.isEmpty();

		if (!MyObjects.equals(this.key, key)) {
			String oldKey = this.key;
			this.key = key;
			fireChange(KEY_PROPERTY_NAME, oldKey, this.key);
		}
	}

	public Map<FieldSpecification, Field> getFields() {
		return Collections.unmodifiableMap(this.fields);
	}

	/**
	 * 
	 * @param fields
	 * @return the count of elements effectively added (stops at the first fail).
	 */
	public int setFields(Collection<Field> fields) {
		assert fields!=null && !MyCollections.contains(fields, null);

		for (Field field : this.fields.values()) {
			field.removeFieldListener(this);
		}
		this.fields.clear();
		int addedCount = 0;
		for (Field field : fields) {
			if (!checkedAddField(field)) {
				break;
			}
			addedCount++;
		}
		// TODO what old/new values?
		fireChange(FIELDS_PROPERTY_NAME, null, null);
		return addedCount;
	}

	public boolean addField(Field field) {
		assert field!=null;
		assert field.getSpecification()!=null;

		if (checkedAddField(field)) {
			// TODO what old/new values?
			fireChange(FIELDS_PROPERTY_NAME, null, null);
			return true;
		}
		return false;
	}

	public boolean removeField(FieldSpecification specification) {
		assert specification!=null;

		if (checkedRemoveField(specification)) {
			// TODO what old/new values?
			fireChange(FIELDS_PROPERTY_NAME, null, null);
			return true;
		}
		return false;
	}

	private boolean checkedAddField(Field field) {
		assert field!=null;
		assert field.getSpecification()!=null;

		if (!this.fields.containsKey(field.getSpecification())) {
			this.fields.put(field.getSpecification(), field);
			field.addFieldListener(this);
			return true;
		}
		return false;
	}

	private boolean checkedRemoveField(FieldSpecification specification) {
		assert specification!=null;

		Field field = this.fields.get(specification);
		if (field!=null) {
			field.removeFieldListener(this);
			this.fields.remove(specification);
			return true;
		}
		return false;
	}

	public void addEntryListener(EntryListener l) {
		assert l!=null;

		this.listeners.add(EntryListener.class, l);
	}

	public void removeEntryListener(EntryListener l) {
		assert l!=null;

		this.listeners.remove(EntryListener.class, l);
	}

	public EntryListener[] getEntryListeners() {
		return this.listeners.getListeners(EntryListener.class);
	}

	protected void fireChange(String propertyName, Object oldValue, Object newValue) {
		EntryEvent event = new EntryEvent(this, propertyName, oldValue, newValue);
		for (EntryListener listener : getEntryListeners()) {
			listener.entryChanged(event);
		}
	}

	@Override
	public void entryDefinitionChanged(EntryDefinitionEvent event) {
		EntryDefinition entryDefinition = event.getSource();

		assert this.definition == entryDefinition;

		if (EntryDefinition.FIELD_SPECIFICATIONS_PROPERTY_NAME.equals(event.getPropertyName())) {
			// TODO handle entry definition's field specifications change
		}

		// TODO what old/new values?
		fireChange(DEFINITION_PROPERTY_NAME, null, null);
	}

	@Override
	public void fieldChanged(FieldEvent event) {
		Field field = event.getSource();

		assert this.fields.containsKey(field.getSpecification());

		if (Field.SPECIFICATION_PROPERTY_NAME.equals(event.getPropertyName())) {
			// TODO handle field's specification change
		}

		// TODO what old/new values?
		fireChange(FIELDS_PROPERTY_NAME, null, null);
	}

}
